package cs757;

import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Vector;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.swing.*;
import javax.swing.GroupLayout.Alignment;
import javax.swing.LayoutStyle.ComponentPlacement;
import javax.swing.border.EmptyBorder;
import javax.swing.table.TableColumn;

import org.apache.commons.codec.DecoderException;

public class EncryptionGUI extends JFrame {
	private static final long serialVersionUID = -5446802836560506771L;
	
	private JPanel contentPane;
	private JTable table;
	private JScrollPane scrollPane;
	private JButton btnEncryptStudents;
	private JButton btnViewDecryptedStudents;
	
	//Where the encryption key will come from
	private final JFileChooser fileChooser = new JFileChooser();
	
	private DatabaseConnector dbConn;
	
	private EncryptionHandler encryptionHandler;
	
	/**
	 * Launch the application.
	 */
	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					EncryptionGUI frame = new EncryptionGUI();
					frame.setVisible(true);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	/**
	 * Create the frame.
	 */
	public EncryptionGUI() {
		setTitle("University Database Encryption");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setBounds(100, 100, 745, 561);
		contentPane = new JPanel();
		contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(contentPane);
		
		JButton btnProvideEncryptionKey = new JButton("Set Encryption Key");
		btnProvideEncryptionKey.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				int retVal = fileChooser.showOpenDialog(EncryptionGUI.this);

				//TODO: Something smart if the user doesn't select a file, or cancels
		        if (retVal == JFileChooser.APPROVE_OPTION) {
		            File file = fileChooser.getSelectedFile();
		            
		            try {
						encryptionHandler = new EncryptionHandler(readKeyFromFile(file));
						
						enableEncryptDecryptFeatures(true);
					} catch (IOException e) {
						JOptionPane.showMessageDialog(null, "Could not open file " + file.getName() + ".");
					} catch (InvalidKeyException e) {
						JOptionPane.showMessageDialog(null, "The file " + file.getName() + " does not contain a valid DES encryption key.");
					} catch (DecoderException e) {
						//Depending on how exactly the key is invalid, this error gets thrown as well.
						JOptionPane.showMessageDialog(null, "The file " + file.getName() + " does not contain a valid DES encryption key.");
					} catch (InvalidKeySpecException e) {
						//This shouldn't be possible
						e.printStackTrace();
					} catch (NoSuchAlgorithmException e) {
						//This shouldn't be possible
						e.printStackTrace();
					} catch (NoSuchPaddingException e) {
						//This shouldn't be possible
						e.printStackTrace();
					} catch (InvalidAlgorithmParameterException e) {
						//This shouldn't be possible
						e.printStackTrace();
					}
		        }
			}
		});
		
		btnEncryptStudents = new JButton("Encrypt Students");
		btnEncryptStudents.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				try {
					encryptStudentsTable();
				} catch (SQLException e) {
					JOptionPane.showMessageDialog(null, "Error querying database.\nQuery:"+DatabaseConnector.STUDENTS_QUERY,
							"Error In Query", JOptionPane.ERROR_MESSAGE);
				}
				
			}
		});
		
		JButton btnViewEncryptedStudents = new JButton("View Encrypted Students");
		btnViewEncryptedStudents.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				try {
					Vector<Vector<Object>> resultData = dbConn.executeQuery(DatabaseConnector.ENCRYPTED_STUDENTS_QUERY, 
							DatabaseConnector.columnNamesStudents.size());
					refreshResultsTable(resultData, DatabaseConnector.columnNamesStudents);
				} catch (SQLException e) {
					JOptionPane.showMessageDialog(null, "Error querying database.\nQuery:"+DatabaseConnector.ENCRYPTED_STUDENTS_QUERY,
							"Error In Query", JOptionPane.ERROR_MESSAGE);
				}
			}
		});
		
		btnViewDecryptedStudents = new JButton("View Decrypted Students");
		btnViewDecryptedStudents.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				try {
					Vector<Vector<Object>> resultData = decryptStudentsTable();
					
					if (resultData != null)
					{
						refreshResultsTable(resultData, DatabaseConnector.columnNamesStudents);
					}
				} catch (SQLException e) {
					JOptionPane.showMessageDialog(null, "Error querying database.\nQuery:"+DatabaseConnector.ENCRYPTED_STUDENTS_QUERY,
							"Error In Query", JOptionPane.ERROR_MESSAGE);
				}
			}
		});
		
		JLabel lblResults = new JLabel("Results:");
		lblResults.setFont(new Font("Tahoma", Font.PLAIN, 14));
		
		scrollPane = new JScrollPane();
		
		GroupLayout gl_contentPane = new GroupLayout(contentPane);
		gl_contentPane.setHorizontalGroup(
			gl_contentPane.createParallelGroup(Alignment.LEADING)
				.addGroup(gl_contentPane.createSequentialGroup()
					.addGroup(gl_contentPane.createParallelGroup(Alignment.LEADING)
						.addGroup(gl_contentPane.createSequentialGroup()
							.addGroup(gl_contentPane.createParallelGroup(Alignment.LEADING)
								.addGroup(gl_contentPane.createSequentialGroup()
									.addContainerGap()
									.addComponent(btnProvideEncryptionKey, GroupLayout.PREFERRED_SIZE, 219, GroupLayout.PREFERRED_SIZE)
									.addPreferredGap(ComponentPlacement.RELATED)
									.addComponent(btnEncryptStudents, GroupLayout.PREFERRED_SIZE, 219, GroupLayout.PREFERRED_SIZE))
								.addGroup(gl_contentPane.createSequentialGroup()
									.addContainerGap()
									.addComponent(btnViewEncryptedStudents, GroupLayout.PREFERRED_SIZE, 219, GroupLayout.PREFERRED_SIZE)							
									.addPreferredGap(ComponentPlacement.RELATED)
									.addComponent(btnViewDecryptedStudents, GroupLayout.PREFERRED_SIZE, 219, GroupLayout.PREFERRED_SIZE)))
							.addGap(72))
						.addGroup(gl_contentPane.createSequentialGroup()
							.addGap(18)
							.addGroup(gl_contentPane.createParallelGroup(Alignment.LEADING)
								.addGroup(gl_contentPane.createSequentialGroup()
									.addComponent(scrollPane, GroupLayout.PREFERRED_SIZE, 584, GroupLayout.PREFERRED_SIZE)
									.addPreferredGap(ComponentPlacement.UNRELATED))
								.addComponent(lblResults))))
					.addGap(2))
		);
		gl_contentPane.setVerticalGroup(
			gl_contentPane.createParallelGroup(Alignment.LEADING)
				.addGroup(gl_contentPane.createSequentialGroup()
					.addGroup(gl_contentPane.createParallelGroup(Alignment.LEADING)
						.addGroup(gl_contentPane.createSequentialGroup()
							.addGroup(gl_contentPane.createParallelGroup(Alignment.BASELINE)
								.addComponent(btnProvideEncryptionKey, GroupLayout.PREFERRED_SIZE, 39, GroupLayout.PREFERRED_SIZE)
								.addComponent(btnEncryptStudents, GroupLayout.PREFERRED_SIZE, 39, GroupLayout.PREFERRED_SIZE))
							.addGroup(gl_contentPane.createParallelGroup(Alignment.BASELINE)
								.addComponent(btnViewEncryptedStudents, GroupLayout.PREFERRED_SIZE, 39, GroupLayout.PREFERRED_SIZE)
								.addComponent(btnViewDecryptedStudents, GroupLayout.PREFERRED_SIZE, 39, GroupLayout.PREFERRED_SIZE))))
					.addGap(18)
					.addComponent(lblResults)
					.addPreferredGap(ComponentPlacement.RELATED)
					.addGroup(gl_contentPane.createParallelGroup(Alignment.BASELINE)
						.addComponent(scrollPane, GroupLayout.DEFAULT_SIZE, 381, Short.MAX_VALUE)))
		);
		
		table = new JTable();
		scrollPane.setViewportView(table);
		contentPane.setLayout(gl_contentPane);
		
		enableEncryptDecryptFeatures(false);
	}
	
	public void launch(DatabaseConnector dbConn){
		this.dbConn = dbConn;
		setVisible(true);
	}
	
	private void refreshResultsTable(Vector<Vector<Object>> results, Vector<String> columnNames){
		table = new JTable(results, columnNames){
			private static final long serialVersionUID = 1929503698784343200L;

			public boolean isCellEditable(int rowIndex, int vColIndex) {
		        return false;
		    }
		};	
		table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
		TableColumn column = null;
		for (int i = 0; i < columnNames.size(); i++) {
		    column = table.getColumnModel().getColumn(i);
		    column.setPreferredWidth(110); 
		}
		scrollPane.setViewportView(table);
	}

	/**
	 * Allows dynamically enabling / disabling features that require an
	 * encryption key to be provided.
	 * @param enabled - Whether the features requiring a key file should be enabled (true) or not
	 */
	private void enableEncryptDecryptFeatures(boolean enabled){
		btnEncryptStudents.setEnabled(enabled);
		btnViewDecryptedStudents.setEnabled(enabled);
	}
	
	/**
	 * Reads the graduates table, and encrypts the ages, and writes the tuples to
	 * the graduatesencrypted table. Anything currently in the graduatesencrypted table
	 * will not be preserved.
	 * @throws SQLException
	 */
	private void encryptStudentsTable() throws SQLException{
		//Clear a spot for encrypted data
		dbConn.executeDropEncryptedStudents();
		dbConn.executeCreateEncryptedStudents();
		
		//Read in the students
		Vector<Vector<Object>> resultData = dbConn.executeQuery(DatabaseConnector.STUDENTS_QUERY, 
				DatabaseConnector.columnNamesStudents.size());
			
		//For every student in the student table, encrypt their age, and insert them in to the encrypted students table
		for (Vector<Object> result:resultData) {
			Object[] encryptedStudentEntry = result.toArray();
			try {
				encryptedStudentEntry[1] = encryptionHandler.encodeInt(Integer.parseInt((String) encryptedStudentEntry[1]));
				
				//None of the below errors should be possible since they would be caused by an invalid DES key, which would
				//have been caught when the EncryptionHandler class was instantiated.
			} catch (NumberFormatException e) {
				e.printStackTrace();
			} catch (IllegalBlockSizeException e) {
				e.printStackTrace();
			} catch (BadPaddingException e) {
				e.printStackTrace();
			} //Encrypt age
				
			dbConn.executeInsert(DatabaseConnector.INSERT_ENCRYPTED_STUDENT_QUERY, encryptedStudentEntry);
		}
	}
	
	/**
	 * Reads the graduatesencrypted table, and decrypts the results so they can be displayed
	 * @return  Tuples from graduatesencrypted table with ages decrypted
	 * @throws SQLException
	 */
	private Vector<Vector<Object>> decryptStudentsTable() throws SQLException{	
		Vector<Vector<Object>> retVal = new Vector<Vector<Object>>();
		
		//Read in the students
		Vector<Vector<Object>> resultData = dbConn.executeQuery(DatabaseConnector.ENCRYPTED_STUDENTS_QUERY, 
				DatabaseConnector.columnNamesStudents.size());
			
		//For every student in the student table, encrypt their age, and insert them in to the encrypted students table
		for (Vector<Object> result:resultData) {
			Object[] studentEntry = result.toArray();
			try {
				studentEntry[1] = encryptionHandler.decodeInt((String) studentEntry[1]);
				
				//All of the below errors may occur if the given key does not match the one used for encryption.
			} catch (IllegalBlockSizeException e) {
				JOptionPane.showMessageDialog(null, "The provided key does not match the one used for encryption.");
				return null;
			} catch (BadPaddingException e) {
				JOptionPane.showMessageDialog(null, "The provided key does not match the one used for encryption.");
				return null;
			} catch (DecoderException e) {
				JOptionPane.showMessageDialog(null, "The provided key does not match the one used for encryption.");
				return null;
			} //Decrypt age
				
			//TODO: This is a gross way to do this
			retVal.add(new Vector<Object>(Arrays.asList(studentEntry)));
		}
		
		return retVal;
	}
	
	/**
	 * Read a DES key from the given file.
	 * @param keyFile - The file to read the key from
	 * @return  The key read in from the given key file
	 * @throws IOException
	 */
	private String readKeyFromFile(File keyFile) throws IOException{
		String retVal = "";
		
		FileInputStream stream = new FileInputStream(keyFile);
		
		FileChannel fileChannel = stream.getChannel();
		MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
		
		retVal = Charset.defaultCharset().decode(buffer).toString();
		
		stream.close();
		
		retVal = retVal.trim();
		//TODO: Throw error if result is not 64 bit hexadecimal key?
		
		return retVal;
	}
}
